<?php
/**
 * This file houses the Model class.
 *
 * Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this list
 *   of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, this 
 *   list of conditions and the following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 * - Neither the name of the Kalkomey Enterprises, Inc. nor the names of its 
 *   contributors may be used to endorse or promote products derived from this 
 *   software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD License
 * @copyright Copyright (c) 2010, Kalkomey Enterprises, Inc.
 * @link http://code.google.com/p/swerv
 * @package	Framework
 * @subpackage Library
 */

/**
 * The Model class provides basic database access features and converts database tables to an object model, following the ActiveRecord design.
 *
 * @package	Framework
 * @subpackage Library
 */
class MysqlModel implements Iterator {

	/**
	 * The name of the table the record is stored into.
	 *
	 * @var string
	 */
	public static $tableName;

	/**
	 * The name of the primary key field in the database table.
	 *
	 * @var string
	 */
	public static $idField;

	/**
	 * A list of fields that have been modified; only these will be saved.
	 *
	 * @var array
	 */
	protected $dirtyFields;

	/**
	 * An array which holds all of the properties of the record.
	 *
	 * @var array
	 */
	protected $properties;

	/**
	 * A list of field names in the database table.  Properties that do not exist in the database table are not saved back to the database.
	 *
	 * @var array
	 */
	protected $tableFields;

	/**
	 * If set to true, then the table must have a deleted_date column (DATETIME type) which will be populated when the field is "Deleted".  Otherwise, the record will actually be removed from the database.
	 *
	 * Will be set to true if a delete_date column is found in the table, otherwise will default to false.
	 *
	 * @var bool
	 */
	private $actsAsParanoid;

	/**
	 * Collection of Relation objects which define has-one or has-many relationships.
	 *
	 * @var array
	 */
	protected $relationsHasOneOrMany;

	/**
	 * Collection of Relation objects which define belongs-to relationships.
	 *
	 * @var array
	 */
	protected $relationsBelongsTo;

	/**
	 * Collection of Relation objects which define belongs-to-has-many relationships.
	 *
	 * @var array
	 */
	protected $relationsBelongsToHasMany;

	/**
	 * Collection of Behavior objects which can be used anywhere in the model.
	 *
	 * @var array
	 */
	private $behaviors;

	/**
	 * Collection of Filter objects which can be used to ensure data is valid before it is saved.
	 *
	 * @var array
	 */
	private $filters;

	private $iterationIndex;

	/**
	 * The object constructor.
	 * Will call the Init() function if it is defined in the child object
	 *
	 * @uses MysqlModel::determineTableName()
	 * @uses MysqlModel::fetchTableStructure()
	 *
	 * @param int $id the value of the primary ID field if you want to preload this model with data
	 *
	 * @return Model
	 */
	public function __construct($id = null)
	{
		if (self::$tableName < 1) {
			self::$tableName = null;
		}
		if (strlen(self::$idField) < 1) {
			self::$idField = null;
		}
		$this->dirtyFields = array();
		$this->properties = array();
		$this->tableFields = array();
		if (!$this->actsAsParanoid === true) {
			$this->actsAsParanoid = false;
		}
		$this->relationsHasOneOrMany = array();
		$this->relationsBelongsTo = array();
		$this->relationsBelongsToHasMany = array();
		$this->filters = array();
		$this->behaviors = array();
		if (method_exists($this, 'init')) {
			$this->init();
		}
		$this->determineTableName();
		$struc = self::fetchTableStructure();
		if (self::$idField == null) {
			self::$idField = $struc['idField'];
		}
		$this->tableFields = $struc['fields'];
		$this->properties = $struc['defaults'];
		$this->actsAsParanoid = $struc['actsAsParanoid'];
		if ($id !== null) {
			$this->fillBy(self::$idField, $id);
		}
	}

	/**
	 * Dynamically determines the name of the table associated with this model by pluralizing the name of the class and converting it to lower case with underscores between the words.
	 *
	 * @uses Inflector::pluralize()
	 *
	 * @return void
	 */
	private function determineTableName()
	{
		if (self::$tableName == null) {
			if (get_class($this) == 'Model') {
				throw new Exception('You cannot create a new instance of the Model class.  Use the static functions or extend it.');
			}
			self::$tableName = Inflector::pluralize(StringUtility::toLowerCase(get_class($this)));
		}
	}

	/**
	 * Returns the table name associated with this model.
	 *
	 * @uses Inflector::pluralize()
	 *
	 * @return string
	 */
	public static function getTableName() {
		return Inflector::pluralize(StringUtility::toLowerCase(get_called_class()));
	}

	/**
	 * Looks in memcache for a saved table structure; if none found, reads it from MySQL.
	 * Returns an array with the following keys:
	 * 1. idField : the name of the field that serves as the primary key
	 * 2. actsAsParanoid : whether or not soft-delete is used (determined by existence of deleted_date)
	 * 3. fields : an array of all the field names
	 * 4. defaults : a key / value array of fields with their default values
	 * 5. tableName : the name of the table based on the class name
	 *
	 * @uses Cache::getInstance()
	 * @uses MysqlModel::getTableName()
	 * @uses MysqlModel::doQuery()
	 *
	 * @throws Exception if no primary key is defined.
	 *
	 * @return array
	 */
	public static function fetchTableStructure() {
		// determine table name
		if (get_called_class() == 'Model') {
			throw new Exception('This method cannot be used from the MysqlModel class.  The Model class must be extended and you can call it from the child class.');
		}
		$tableName = self::getTableName();
		$idField = null;
		$actsAsParanoid = false;
		$fields = array();
		$defaults = array();

		// look in memcache for the data
		$cache = Cache::getInstance(60*60*24);
		$cacheIdField = 'model_idfield_' . $tableName;
		$cacheActsAsParanoid = 'model_actsasparanoid_' . $tableName;
		$cacheFields = 'model_fields_' . $tableName;
		$cacheDefaults = 'model_defaults_' . $defaults;
		try {
			$idField = $cache->$cacheIdField;
			$actsAsParanoid = $cache->$cacheActsAsParanoid;
			$fields = $cache->$cacheFields;
			$defaults = $cache->$cacheDefaults;
		} catch (Exception $e) {
			$result = self::doQuery('DESCRIBE `' . $tableName . '`', true);
			while ($row = $result->fetch_object()) {
				if ($row->Key == 'PRI' && $idField == null) {
					$idField = $row->Field;
				}
				if ($row->Field == 'deleted_date' && strcasecmp($row->Type, 'datetime') == 0) {
					$actsAsParanoid = true;
				}
				if (strcasecmp($row->Null, 'no') == 0 && !empty($row->Default)) {
					$defaults[$row->Field] = $row->Default;
				}
				$fields[] = $row->Field;
			}

			try	{
				$cache->$cacheIdField = $idField;
				$cache->$cacheActsAsParanoid = $actsAsParanoid;
				$cache->$cacheFields = $fields;
				$cache->$cacheDefaults = $defaults;
			} catch (CacheDisabledException $e)	{
				// do nothing
			}
		}
		$returnVal = array (
			'idField' => $idField,
			'actsAsParanoid' => $actsAsParanoid,
			'fields' => $fields,
			'defaults' => $defaults,
			'tableName' => $tableName
		);
		return $returnVal;
	}

	/**
	 * Converts a property name in thisNotation to a field name in this_notation.
	 *
	 * @uses StringUtility::toLowerCase()
	 *
	 * @param string $name
	 *
	 * @return string
	 */
	public static function getFieldName($name) {
		return StringUtility::toLowerCase(ucfirst($name));
	}

	/**
	 * Override of PHP magic method.  Returns the value for a property if it exists or returns null.
	 *
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::isValidProperty()
	 * @uses MysqlModel::loadBelongsTo()
	 * @uses MysqlModel::loadHasOneOrMany()
	 * @uses MysqlModel::loadBelongsToHasMany()
	 *
	 * @param string $name
	 *
	 * @return mixed|null
	 */
	public function __get($name) {
		$returnValue = null;
		if ($this->isValidProperty($name)) {
			$fieldName = self::getFieldName($name);
			if (array_key_exists($fieldName, $this->properties)) {
				$returnValue = $this->properties[$fieldName];
			} else if (array_key_exists($fieldName, $this->relationsBelongsTo)) {
				$this->loadBelongsTo($name);
				$returnValue = $this->properties[$fieldName];
			} else if (array_key_exists($fieldName, $this->relationsHasOneOrMany)) {
				$this->loadHasOneOrMany($name);
				$returnValue = $this->properties[$fieldName];
			} else if (array_key_exists($fieldName, $this->relationsBelongsToHasMany)) {
				$this->loadBelongsToHasMany($name);
				$returnValue = $this->properties[$fieldName];
			}
		} else if (array_key_exists($name, $this->behaviors)) {
			$returnValue = $this->behaviors[$name];
		} else if (array_key_exists($name, $this->filters)) {
			$returnValue = $this->filters[$name];
		}
		return $returnValue;
	}

	/**
	 * Adds a behavior that can then be accessed anywhere in the model by the $name.
	 *
	 * @param string $name the name of the behavior; ex: Encryption will load the EncryptionBehavior class
	 *
	 * @return void
	 */
	public function addBehavior($name) {
		$className = $name . 'Behavior';
		$obj = new $className();
		$this->behaviors[$name] = $obj;
	}

	/**
	 * Adds a filter to the model; data is checked against the filter when set (not when saved) and will be rejected if the filter fails.
	 *
	 * @param string $propertyName the property to add this filter to
	 * @param Filter $filter
	 *
	 * @return void
	 */
	public function addFilter($propertyName, Filter $filter) {
		if (!isset($this->filters[$propertyName])) {
			$this->filters[$propertyName] = array();
		}
		$this->filters[$propertyName][] = $filter;
	}

	/**
	 * Override of PHP magic method.  Sets a property to the given value and marks the field as dirty.
	 *
	 * @uses MysqlModel::isValidProperty()
	 * @uses MysqlModel::getFieldName()
	 *
	 * @throws Exception if the field does not exist in the table structure.
	 * @throws FilterException if the field has data which does not match a particular filter
	 *
	 * @param string $name
	 * @param mixed $value
	 *
	 * @return void
	 */
	public function __set($name, $value) {
		$fieldName = self::getFieldName($name);
		if ($this->isValidProperty($name)) {
			if (array_key_exists($name, $this->filters)) {
				$errors = array();
				foreach ($this->filters[$name] as $filter) {
					$result = $filter->Check($value);
					if ($result !== null) {
						$errors[] = $result;
					}
				}
				if (count($errors) > 0) {
					throw new FilterException($errors);
				}
			}
			$this->dirtyFields[] = $fieldName;
			$this->properties[$fieldName] = $value;
		}
		else {
			throw new Exception('Invalid property "' . $name . '".  Field "' . $fieldName . '" does not exist in the corresponding database table "' . self::$tableName . '"');
		}
	}

	/**
	 * Determines if a property exists in the model's table structure.
	 *
	 * @uses MysqlModel::getFieldName()
	 *
	 * @param string $name
	 *
	 * @return bool
	 */
	public function isValidProperty($name) {
		$returnValue = false;
		$fieldName = self::getFieldName($name);
		if (in_array($fieldName, $this->tableFields)) {
			$returnValue = true;
		} else if (array_key_exists($name, $this->relationsHasOneOrMany)) {
			$returnValue = true;
		} else if (array_key_exists($name, $this->relationsBelongsTo)) {
			$returnValue = true;
		} else if (array_key_exists($name, $this->relationsBelongsToHasMany)) {
			$returnValue = true;
		}
		return $returnValue;
	}

	/**
	 * Returns the value for the primary key field for this model or null if it isn't set yet.
	 *
	 * @return mixed
	 */
	public function getId()
	{
		$returnValue = null;
		if (array_key_exists(self::$idField, $this->properties)) {
			$returnValue = $this->properties[self::$idField];
		}
		return $returnValue;
	}

	/**
	 * Overrides PHP magic method.
	 *
	 * @uses MysqlModel::isValidProperty()
	 *
	 * @param string $name
	 *
	 * @return bool
	 */
	public function __isset($name) {
		return $this->isValidProperty($name);
	}

	/**
	 * Overrides PHP magic method.
	 *
	 * @uses MysqlModel::findBy()
	 * @uses MysqlModel::fillBy()
	 *
	 * @throws Exception if method is undefined.
	 *
	 * @param string $name
	 * @param array $arguments
	 *
	 * @return mixed
	 */
	public function __call($name, $arguments) {
		// findBy?
		if (stripos($name, 'findBy') !== false)
		{
			$propertyName = substr($name, 6, strlen($name));
			$searchValue = null;
			$maxTotalRecords = null;
			$startRecord = 0;
			$orderBy = null;
			if (isset($arguments[0])) {
				$searchValue = $arguments[0];
			}
			if (isset($arguments[1])) {
				$orderBy = $arguments[1];
			}
			if (isset($arguments[2])) {
				$maxTotalRecords = $arguments[2];
			}
			if (isset($arguments[3])) {
				$startRecord = $arguments[3];
			}
			return $this->findBy($propertyName, $searchValue, $orderBy, $maxTotalRecords, $startRecord);
		} else if (stripos($name, 'fillBy') !== false) { // fillBy?
			$propertyName = substr($name, 6, strlen($name));
			$searchValue = null;
			if (isset($arguments[0])) {
				$searchValue = $arguments[0];
			} else {
				throw new Exception('Cannot fill by property "' . $name . '".  No search value provided.');
			}
			return $this->fillBy($propertyName, $searchValue);
		} else {
			throw new Exception('Call to undefined method "' . $name . '".');
		}
	}

	/**
	 * Overrides PHP magic method.
	 *
	 * @uses MysqlModel::findAll()
	 *
	 * @param string $name
	 * @param array $arguements
	 *
	 * @return mixed
	 */
	public static function __callStatic($name, $arguments) {
		// findAllBy?
		if (stripos($name, 'findAllBy') !== false)
		{
			$propertyName = substr($name, 9, strlen($name));
			$searchValue = null;
			$maxTotalRecords = null;
			$startRecord = 0;
			$orderBy = null;
			if (isset($arguments[0])) {
				$searchValue = $arguments[0];
			}
			if (isset($arguments[1])) {
				$orderBy = $arguments[1];
			}
			if (isset($arguments[2])) {
				$maxTotalRecords = $arguments[2];
			}
			if (isset($arguments[3])) {
				$startRecord = $arguments[3];
			}
			return self::findAll($propertyName, $searchValue, $orderBy, $maxTotalRecords, $startRecord);
		}
	}

	/**
	 * Fills the object with the data in the array.  The keys must be the field names or property names.  This will NOT mark the object as dirty, so you will need to manually save if it if that is the behavior you are expecting.
	 *
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::isValidProperty()
	 *
	 * @param array $dataArr
	 *
	 * @return void
	 */
	public function fillWith($dataArr, $skipFilters = false) {
		$errors = array();
		foreach ($dataArr as $key => $value) {
			if (!$this->isValidProperty($key)) {
				continue;
			}
			try {
				if ($skipFilters) {
					$field = self::getFieldName($key);
					$this->properties[$key] = $value;
				} else {
					$this->__set($key, $value);
				}
			} catch (FilterException $e) {
				$errors = array_merge($errors, $e->errors);
			}
		}
		if (count($errors) > 0) {
			throw new FilterException($errors);
		}
	}

	/**
	 * Returns a single model or an array of models given specific search parameters.
	 *
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::isValidProperty()
	 * @uses MysqlModel::afterLoad()
	 * @uses MysqlModel::doQuery()
	 *
	 * @throws Exception if trying to find by a property that does not exist
	 *
	 * @param name $propertyName the name of the field to search by
	 * @param name $searchValue the value of the field to search by
	 * @param name $orderBy the property name to sort the recordset by
	 * @param int $maxTotalRecords if null, all available models that match the search terms will returned, otherwise only this number
	 * @param int $startRecord the index of the record to start with if returning multiple records
	 *
	 * @return Model|array
	 */
	public function findBy($propertyName, $searchValue = null, $orderBy = null, $maxTotalRecords = null, $startRecord = null) {
		$returnVal = array();
		$fieldName = self::getFieldName($propertyName);
		if (!$this->isValidProperty($propertyName)) {
			throw new Exception('Cannot find by property "' . $propertyName . '".  Field "' . $fieldName . '" does not exist in the corresponding database table "' . self::$tableName . '"');
		}
		$sql = 'SELECT ';
		foreach ($this->tableFields as $field) {
			$sql .= '`' . $field . '`, ';
		}
		$sql = substr($sql, 0, strlen($sql) - 2);
		$sql .= " FROM `" . self::$tableName . "`";
		if ($searchValue !== null) {
			$sql .= " WHERE `{$fieldName}` = '" . mysql_escape_string($searchValue) . "'";
			if ($this->actsAsParanoid) {
				$sql .= " AND `deleted_date` IS NULL";
			}
		} else if ($this->actsAsParanoid) {
			$sql .= " WHERE `deleted_date` IS NULL";
		}
		if ($orderBy !== null) {
			$orderByField = self::getFieldName($orderBy);
			$sql .= " ORDER BY `{$orderByField}`";
		}
		if ($maxTotalRecords !== null && $startRecord === null) {
			$sql .= " LIMIT {$maxTotalRecords}";
		} else if ($maxTotalRecords !== null && $startRecord !== null) {
			$sql .= " LIMIT {$maxTotalRecords},{$startRecord}";
		}
		$className = get_class($this);
		$resultObj = self::doQuery($sql, true);
		if ($resultObj->num_rows > 1) {
			while($results = $resultObj->fetch_assoc()) {
				$obj = new $className();
				$obj->fillWith($results, true);
				if (method_exists($obj, 'afterLoad')) {
					$obj->afterLoad();
				}
				$returnVal[] = $obj;
			}
		} else if ($resultObj->num_rows == 1) {
			$returnVal = new $className();
			$returnVal->fillWith($resultObj->fetch_assoc(), true);
			if (method_exists($returnVal, 'afterLoad')) {
				$obj->afterLoad();
			}
		} else {
			$returnVal = null;
		}
		return $returnVal;
	}

	/**
	 * Returns a single model or an array of models given specific search parameters.  This is the static version of findBy that can be used without having to instantiate a model.
	 *
	 * @uses MysqlModel::fetchTableStructure()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::fillWith()
	 * @uses MysqlModel::afterLoad()
	 *
	 * @throws Exception if property does not exist in table
	 *
	 * @param string $modelName the name of the model
	 * @param string $propertyName the name of the field to search by
	 * @param string $searchValue the value of the field to search by
	 * @param string $orderBy the property name of the field to order the result set
	 * @param int $maxTotalRecords if null, all available models that match the search terms will returned, otherwise only this number
	 * @param int $startRecord the index of the record to start with if returning multiple records
	 *
	 * @return Model|array
	 */
	public static function findAll($propertyName = null, $searchValue = null, $orderBy = null, $maxTotalRecords = null, $startRecord = 0) {
		$returnVal = array();
		$struc = self::fetchTableStructure();
		$sql = 'SELECT ';
		foreach ($struc['fields'] as $field) {
			$sql .= '`' . $field . '`, ';
		}
		$sql = substr($sql, 0, strlen($sql) - 2);
		$sql .= " FROM `{$struc['tableName']}`";
		if ($propertyName !== null && $searchValue !== null) {
			$fieldName = self::getFieldName($propertyName);
			if (!in_array($fieldName, $struc['fields'])) {
				throw new Exception('Cannot find by property "' . $propertyName . '".  Field "' . $fieldName . '" does not exist in the corresponding database table "' . $struc['tableName'] . '"');
			}
			$sql .= " WHERE `{$fieldName}` = '" . mysql_escape_string($searchValue) . "'";
		}
		if ($struc['actsAsParanoid']) {
			$sql .= " AND `deleted_date` IS NULL";
		}
		if ($orderBy !== null) {
			$sql .= " ORDER BY {$orderBy}";
		}
		if ($maxTotalRecords !== null && $startRecord === null) {
			$sql .= " LIMIT {$maxTotalRecords}";
		} else if ($maxTotalRecords !== null && $startRecord !== null) {
			$sql .= " LIMIT {$maxTotalRecords},{$startRecord}";
		}
		$resultObj = self::doQuery($sql);
		$className = get_called_class();
		if ($resultObj->num_rows > 0) {
			if ($resultObj->num_rows > 1) {
				while($results = $resultObj->fetch_assoc()) {
					$obj = new $className();
					$obj->fillWith($results, true);
					if (method_exists($obj, 'afterLoad')) {
						$obj->afterLoad();
					}
					$returnVal[] = $obj;
				}
			} else if ($resultObj->num_rows == 1) {
				$returnVal = new $className();
				$returnVal->fillWith($resultObj->fetch_assoc(), true);
				if (method_exists($returnVal, 'afterLoad')) {
					$returnVal->afterLoad();
				}
			} else {
				$returnVal = null;
			}
		}
		return $returnVal;
	}

	/**
	 * Fills the current model with a single record that matches given search parameters.
	 * Returns true if the record is found and the model is filled with data; false otherwise.
	 *
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::isValidProperty()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::afterLoad()
	 *
	 * @throws Exception if property does not exist in the table.
	 *
	 * @param name $propertyName the name of the field to search by
	 * @param name $searchValue the value of the field to search by
	 *
	 * @return bool
	 */
	public function fillBy($propertyName, $searchValue) {
		$returnVal = false;
		$fieldName = self::getFieldName($propertyName);
		if (!$this->isValidProperty($propertyName)) {
			throw new Exception('Cannot fill by property "' . $propertyName . '".  Field "' . $fieldName . '" does not exist in the corresponding database table "' . self::$tableName . '"');
		}
		$sql = 'SELECT ';
		foreach ($this->tableFields as $field) {
			$sql .= '`' . $field . '`, ';
		}
		$sql = substr($sql, 0, strlen($sql) - 2);
		$sql .= " FROM `" . self::$tableName . "` WHERE `{$fieldName}` = '" . mysql_escape_string($searchValue) . "'";
		if ($this->actsAsParanoid) {
			$sql .= " AND `deleted_date` IS NULL";
		}
		$sql .= " LIMIT 0,1";
		$resultObj = self::doQuery($sql);
		if ($resultObj->num_rows == 1) {
			$results = $resultObj->fetch_assoc();
			foreach ($results as $key => $val) {
				if (in_array($key, $this->tableFields)) {
					$this->properties[$key] = $val;
				}
			}
			$returnVal = true;
		}
		if (method_exists($this, 'afterLoad')) {
			$this->afterLoad();
		}
		return $returnVal;
	}

	/**
	 * Loads a model that exists in a "belongs to" relationship with this model.
	 *
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::getFieldName()
	 *
	 * @param string $propertyName the name of the property being used to access the related model
	 *
	 * @return void
	 */
	private function loadBelongsTo($propertyName) {
		if (!array_key_exists($propertyName, $this->relationsBelongsTo)) {
			return;
		}
		if (!$this->isExistingRecord()) {
			return;
		}
		$relation = $this->relationsBelongsTo[$propertyName];
		$fieldName = StringUtility::toLowerCase($propertyName);
		$foreignModel = $relation->modelName;
		$foreignKey = $relation->myForeignKey;
		$foreignField = self::getFieldName($foreignKey);
		$fillMethod = 'fillById';
		$obj = new $foreignModel();
		$obj->$fillMethod($this->properties[$foreignField]);
		$this->properties[self::getFieldName($propertyName)] = $obj;
		return;
	}

	/**
	 * Loads a model or models that exist in a "has one" or "has many" relationship with this model.
	 *
	 * @uses StringUtility::toLowerCase()
	 * @uses StringUtility::toPascalCase()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::getId()
	 * @uses MysqlModel::isExistingRecord()
	 *
	 * @param string $propertyName the name of the property being used to access the related model(s)
	 *
	 * @return void
	 */
	private function loadHasOneOrMany($propertyName) {
		if (!array_key_exists($propertyName, $this->relationsHasOneOrMany)) {
			return;
		}
		if (!$this->isExistingRecord()) {
			return;
		}
		$relation = $this->relationsHasOneOrMany[$propertyName];
		$fieldName = StringUtility::toLowerCase($propertyName);
		$foreignModel = $relation->modelName;
		$foreignKey = $relation->theirForeignKey;
		$foreignField = self::getFieldName($foreignKey);
		$fillMethod = 'findAllBy' . StringUtility::toPascalCase($foreignField);
		$models = $foreignMysqlModel::$fillMethod($this->getId());
		$this->properties[self::getFieldName($propertyName)] = $models;
		return;
	}

	/**
	 * Loads a model or models that exist in a "belongs to and has many" relationship with this model.
	 *
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::getId()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::isExistingRecord()
	 *
	 * @param string $propertyName the name of the property being used to access the related model(s)
	 *
	 * @return void
	 */
	private function loadBelongsToHasMany($propertyName) {
		if (!array_key_exists($propertyName, $this->relationsBelongsToHasMany)) {
			return;
		}
		if (!$this->isExistingRecord()) {
			return;
		}
		$relation = $this->relationsBelongsToHasMany[$propertyName];
		$fieldName = StringUtility::toLowerCase($propertyName);
		$foreignModel = $relation->modelName;
		$theirForeignKey = $relation->theirForeignKey;
		$myForeignKey = $relation->myForeignKey;
		$linkTable = $relation->linkTable;
		$theirForeignField = self::getFieldName($theirForeignKey);
		$myForeignField = self::getFieldName($myForeignKey);
		$myId = $this->getId();
		$sql = "SELECT `{$theirForeignField}` FROM `{$linkTable}` WHERE `{$myForeignField}` = '{$myId}'";
		$results = self::doQuery($sql);
		$propertyField = self::getFieldName($propertyName);
		if ($results != false) {
			$this->properties[$propertyField] = array();
			if ($results->num_rows > 0) {
				while ($obj = $results->fetch_object()) {
					$this->properties[$propertyField][] = new $foreignModel($obj->$theirForeignField);
				}
			}
		}
		return;
	}

	/**
	 * Returns whether or not any data in the model has changed but has not yet been saved.
	 *
	 * @return bool
	 */
	public function isDirty() {
		$returnVal = false;
		if (count($this->dirtyFields) > 0) {
			$returnVal = true;
		}
		return $returnVal;
	}

	/**
	 * Defines a belongs-to relationship; the foreign key must be a property in this model that relates to the ID of the related model.
	 *
	 * @uses StringUtility::toCamelCase()
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::getFieldName()
	 * @uses Relation
	 *
	 * @throws Exception if the property name is already in use
	 *
	 * @param string $modelName the name of the model this one is related to
	 * @param string $propertyName the name of the property in thisCase that will be used to access the related model
	 * @param string $foreignKey the name of the property in thisCase in this model that relates to the ID of the related model
	 *
	 * @return void
	 */
	public function belongsTo($modelName, $propertyName = null, $foreignKey = null) {
		if ($propertyName == null) {
			$propertyName = StringUtility::toCamelCase(StringUtility::toLowerCase($modelName));
		}
		if ($foreignKey == null) {
			$foreignKey = StringUtility::toCamelCase(StringUtility::toLowerCase($modelName . 'Id'));
		}
		$fieldName = self::getFieldName($propertyName);
		if (in_array($fieldName, $this->tableFields) || in_array($propertyName, $this->relationsHasOneOrMany) || in_array($propertyName, $this->relationsBelongsToHasMany)) {
			throw new Exception('Invalid property name declared for belongs-to relationship: it is already in use as another relationship or is a field in the database table.');
		}
		$relation = new Relation();
		$relation->modelName = $modelName;
		$relation->myForeignKey = $foreignKey;
		$this->relationsBelongsTo[$propertyName] = $relation;
	}

	/**
	 * Defines a has-one relationship with another model where the foreign key in the related model equals the id of this model.
	 *
	 * @uses StringUtility::toCamelCase()
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::hasOneOrMany()
	 *
	 * @param string $modelName the name of the model this one is related to
	 * @param string $propertyName the name of the property in thisCase that will be used to access the related model
	 * @param string $foreignKey the name of the property in thisCase in the related model that relates to the ID of this model
	 *
	 * @return void
	 */
	public function hasOne($modelName, $propertyName = null, $foreignKey = null) {
		$myModelName = get_class($this);
		if ($propertyName == null) {
			$propertyName = StringUtility::toCamelCase(StringUtility::toLowerCase($modelName));
		}
		if ($foreignKey == null) {
			$foreignKey = StringUtility::toCamelCase(StringUtility::toLowerCase($myModelName . 'Id'));
		}
		$this->hasOneOrMany($modelName, $propertyName, $foreignKey);
	}

	/**
	 * Defines a has-many relationship with another model where the foreign key in the related model equals the id of this model.
	 *
	 * @uses StringUtility::toCamelCase()
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::hasOneOrMany()
	 * @uses Inflector::pluralize()
	 *
	 * @param string $modelName the name of the model this one is related to
	 * @param string $propertyName the name of the property in thisCase that will be used to access the related model
	 * @param string $foreignKey the name of the property in thisCase in the related model that relates to the ID of this model
	 *
	 * @return void
	 */
	public function hasMany($modelName, $propertyName = null, $foreignKey = null) {
		$myModelName = get_class($this);
		if ($propertyName == null) {
			$propertyName = Inflector::pluralize(StringUtility::toCamelCase(StringUtility::toLowerCase($modelName)));
		}
		if ($foreignKey == null) {
			$foreignKey = StringUtility::toCamelCase(StringUtility::toLowerCase($myModelName . 'Id'));
		}
		$this->hasOneOrMany($modelName, $propertyName, $foreignKey);
	}

	/**
	 * Defines a has-one or has-many relationship with another model where the foreign key in the related model equals the id of this model.
	 *
	 * @uses MysqlModel::getFieldName()
	 * @uses Relation
	 *
	 * @throws Exception if property name is already in use.
	 *
	 * @param string $modelName the name of the model this one is related to
	 * @param string $propertyName the name of the property in thisCase that will be used to access the related model
	 * @param string $foreignKey the name of the property in thisCase in the related model that relates to the ID of this model
	 *
	 * @return void
	 */
	private function hasOneOrMany($modelName, $propertyName, $foreignKey) {
		$fieldName = self::getFieldName($propertyName);
		if (in_array($fieldName, $this->tableFields) || in_array($propertyName, $this->relationsBelongsTo) || in_array($propertyName, $this->relationsBelongsToHasMany)) {
			throw new Exception('Invalid property name declared for belongs-to relationship: it is already in use as another relationship or is a field in the database table.');
		}
		$relation = new Relation();
		$relation->modelName = $modelName;
		$relation->theirForeignKey = $foreignKey;
		$this->relationsHasOneOrMany[$propertyName] = $relation;
	}

	/**
	 * Defines a many-to-many relationship that exists through a linked table
	 *
	 * @uses Inflector::pluralize()
	 * @uses StringUtility::toCamelCase()
	 * @uses StringUtility::toLowerCase()
	 * @uses MysqlModel::getFieldName()
	 * @uses Relation
	 *
	 * @param string $modelName the name of the model this one is related to
	 * @param string $linkTable the name of the table in_this_notation that serves as the link table (it has a foreign key field for this model and the related model)
	 * @param string $propertyName the name of the property in thisCase that will be used to access the related model
	 * @param string $myForeignKey the name of the property in thisCase in the link table that relates to the ID of this model
	 * @param string $theirForeignKey the name of the property in thisCase in the link table that relates to the ID of the related model
	 *
	 * @return void
	 */
	public function belongsToHasMany($modelName, $linkTable, $propertyName = null, $myForeignKey = null, $theirForeignKey = null) {
		if ($propertyName == null) {
			$propertyName = Inflector::pluralize(StringUtility::toCamelCase(StringUtility::toLowerCase($modelName)));
		}
		$fieldName = self::getFieldName($propertyName);
		if (in_array($fieldName, $this->tableFields) || in_array($propertyName, $this->relationsBelongsTo) || in_array($propertyName, $this->relationsHasOneOrMany)) {
			throw new Exception('Invalid property name declared for belongs-to relationship: it is already in use as another relationship or is a field in the database table.');
		}
		if ($myForeignKey == null) {
			$myModelName = get_class($this);
			$myForeignKey = StringUtility::toCamelCase(StringUtility::toLowerCase($myModelName . 'Id'));
		}
		if ($theirForeignKey == null) {
			$theirForeignKey = StringUtility::toCamelCase(StringUtility::toLowerCase($modelName . 'Id'));
		}
		$relation = new Relation();
		$relation->modelName = $modelName;
		$relation->theirForeignKey = $theirForeignKey;
		$relation->myForeignKey = $myForeignKey;
		$relation->linkTable = $linkTable;
		$this->relationsBelongsToHasMany[$propertyName] = $relation;
	}

	/**
	 * Looks through the belongs to relationships, saves any new models, and updates the foreign key in this model so it will be saved by MysqlModel::save()
	 *
	 * @uses MysqlModel::getId()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::isDirty()
	 * @uses MysqlModel::save()
	 *
	 * @return void
	 */
	private function saveBelongsTo() {
		foreach ($this->relationsBelongsTo as $propertyName => $relation) {
			$propertyField = self::getFieldName($propertyName);
			if (in_array($propertyField, $this->dirtyFields)) {
				$relatedObj = $this->properties[$propertyField];
				if (is_object($relatedObj) && $relatedObj instanceof Model) {
					if ($relatedObj->isDirty()) {
						$relatedObj->save();
					}
					$foreignId = $relatedObj->getId();
					$foreignKey = self::getFieldName($relation->myForeignKey);
					$this->properties[$foreignKey] = $foreignId;
					$this->dirtyFields[] = $foreignKey;
				}
			}
		}
	}

	/**
	 * Looks through the has-one or has-many relationships, saves any new models, and updates the foreign key
	 *
	 * @uses MysqlModel::getId()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::save()
	 *
	 * @return void
	 */
	private function saveHasOneOrMany() {
		foreach ($this->relationsHasOneOrMany as $propertyName => $relation) {
			$propertyField = self::getFieldName($propertyName);
			if (in_array($propertyField, $this->dirtyFields)) {
				$relatedObjs = $this->properties[$propertyField];
				if (!is_array($relatedObjs)) {
					$relatedObjs = array($relatedObjs);
				}
				foreach ($relatedObjs as $relatedObj) {
					if (is_object($relatedObj) && $relatedObj instanceof Model) {
						$foreignKey = $relation->theirForeignKey;
						$relatedObj->$foreignKey = $this->getId();
						$relatedObj->save();
					}
				}
			}
		}
	}

	/**
	 * Looks through the belongs-to-has-many relationships and saves any new relationships.
	 *
	 * @uses MysqlModel::getId()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::isDirty()
	 * @uses MysqlModel::save()
	 *
	 * @return void
	 */
	private function saveBelongsToHasMany() {
		foreach ($this->relationsBelongsToHasMany as $propertyName => $relation) {
			$propertyField = self::getFieldName($propertyName);
			if (in_array($propertyField, $this->dirtyFields)) {
				$relatedObjs = $this->properties[$propertyField];
				if (!is_array($relatedObjs)) {
					$relatedObjs = array($relatedObjs);
				}
				foreach ($relatedObjs as $relatedObj) {
					if (is_object($relatedObj) && $relatedObj instanceof Model) {
						if ($relatedObj->isDirty()) {
							$relatedObj->save();
						}
						$linkTable = $relation->linkTable;
						$foreignModel = $relation->modelName;
						$myForeignKey = $relation->myForeignKey;
						$myForeignField = self::getFieldName($myForeignKey);
						$theirForeignKey = $relation->theirForeignKey;
						$theirForeignField = self::getFieldName($theirForeignKey);
						$myId = $this->getId();
						$theirId = $relatedObj->getId();
						$sql = "REPLACE INTO `{$linkTable}` ( `{$myForeignField}`, `{$theirForeignField}` ) VALUES ( '{$myId}', '{$theirId}' )";
						self::doQuery($sql);
					}
				}
			}
		}
	}

	/**
	 * Saves the data to the database table.  If no fields have been changed, no data will be saved.
	 *
	 * @uses MysqlModel::isDirty()
	 * @uses MysqlModel::beforeSave()
	 * @uses MysqlModel::afterSave()
	 * @uses MysqlModel::buildInsertQuery()
	 * @uses MysqlModel::buildUpdateQuery()
	 * @uses MysqlModel::isExistingRecord()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::saveBelongsTo()
	 * @uses MysqlModel::saveHasOneOrMany()
	 * @uses MysqlModel::saveBelongsToMany()
	 *
	 * @return void
	 */
	public function save() {
		if (!$this->isDirty()) {
			return;
		}
		if (method_exists($this, 'beforeSave')) {
			$result = $this->beforeSave();
			if (!$result) {
				return false;
			}
		}

		// check for objects in various relationships
		$this->saveBelongsTo();

		// check for id field to see if we are inserting or updating
		if ($this->isExistingRecord()) {
			$sql = $this->buildUpdateQuery();
		} else {
			$sql = $this->buildInsertQuery();
		}
		if ($sql !== null) {
			$results = self::doQuery($sql);
			if (!$this->isExistingRecord()) {
				$this->properties[self::$idField] = $mysqli->insert_id;
			}
		}

		// now we can save any related has-one or has-many objects
		$this->saveHasOneOrMany();

		// now we can save any belongs-to-has-many objects
		$this->saveBelongsToHasMany();

		$this->dirtyFields = array();
		if (method_exists($this, 'afterSave')) {
			$this->afterSave();
		}
		return;
	}

	/**
	 * Builds the query to insert a new record.
	 *
	 * @return string
	 */
	private function buildInsertQuery() {
		$fields = '';
		$values = '';
		$sql = "INSERT INTO `" . self::$tableName . "`";
		foreach ($this->dirtyFields as $fieldName) {
			if ($fieldName == 'updated_date' || $fieldName == 'created_date' || $fieldName == 'deleted_date' || !array_key_exists($fieldName, $this->properties) || !in_array($fieldName, $this->tableFields)) {
				continue;
			} else {
				$fields .= "`{$fieldName}`, ";
				$values .= "'" . mysql_escape_string($this->properties[$fieldName]) . "', ";
			}
		}
		if ($fields == '' && $values == '') {
			return null;
		}
		if (in_array('created_date', $this->tableFields)) {
			$fields .= '`created_date`, ';
			$values .= 'NOW(), ';
		}
		if (in_array('updated_date', $this->tableFields)) {
			$fields .= '`updated_date`, ';
			$values .= 'NOW(), ';
		}
		$fields = substr($fields, 0, strlen($fields) - 2);
		$values = substr($values, 0, strlen($values) - 2);
		$sql .= " ({$fields}) VALUES ({$values})";
		return $sql;
	}

	/**
	 * Builds the query to update an existing record.
	 *
	 * @uses MysqlModel::getId()
	 *
	 * @return string
	 */
	private function buildUpdateQuery() {
		$isOk = true;
		$sql = "UPDATE `" . self::$tableName . "` SET ";
		foreach ($this->dirtyFields as $fieldName) {
			if ($fieldName == 'updated_date' || $fieldName == 'created_date' || $fieldName == 'deleted_date' || !array_key_exists($fieldName, $this->properties) || $fieldName == self::$idField || !in_array($fieldName, $this->tableFields)) {
				$isOk = false;
				continue;
			}
			$sql .= "`{$fieldName}` = '" . mysql_escape_string($this->properties[$fieldName]) . "', ";
		}
		if (!$isOk) {
			return null;
		}
		if (in_array('updated_date', $this->tableFields)) {
			$sql .= '`updated_date` = NOW(), ';
		}
		$sql = substr($sql, 0, strlen($sql) - 2);
		$sql .= " WHERE `" . self::$idField . "` = '" . mysql_escape_string($this->getId()) . "'";
		return $sql;
	}

	/**
	 * Looks to see if the ID field is set and not empty; if it is, then this record exists in the database; otherwise it doesn't.
	 *
	 * @return bool
	 */
	private function isExistingRecord() {
		return isset($this->properties[self::$idField]) && !empty($this->properties[self::$idField]);
	}

	/**
	 * Deletes the current record from the database.
	 *
	 * @uses MysqlModel::isExistingRecord()
	 * @uses MysqlModel::doQuery()
	 * @uses MysqlModel::getId()
	 *
	 * @return void
	 */
	public function delete() {
		if (!$this->isExistingRecord()) {
			return;
		}
		if ($this->actsAsParanoid) {
			$sql = "UPDATE `" . self::$tableName . "` SET `deleted_date` = NOW()";
		} else {
			$sql = "DELETE FROM `" . self::$tableName . "`";
		}
		$sql .= " WHERE `" . self::$idField . "` = '" . mysql_escape_string($this->getId()) . "'";
		self::doQuery($sql);
		$this->dirtyFields = array();
		return;
	}

	/**
	 * Removes all records from a table that match the given search parameters.
	 *
	 * @uses MysqlModel::fetchTableStructure()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::doQuery()
	 *
	 * @throws Exception if property name is not defined in the table
	 *
	 * @return void
	 */
	public static function remove($propertyName, $searchValue) {
		$struc = self::fetchTableStructure();
		$fieldName = self::getFieldName($propertyName);
		if (!in_array($fieldName, $struc['fields'])) {
			throw new Exception('Unable to remove records using "'.$propertyName.'" because that field does not exist in the corresponding database table.');
		}
		if ($struc['actsAsParanoid']) {
			$sql = "UPDATE `{$struc['tableName']}` SET `deleted_date` = NOW()";
		} else {
			$sql = "DELETE FROM `{$struc['tableName']}`";
		}
		$sql .= " WHERE `{$fieldName}` = '" . mysql_escape_string($searchValue) . "'";
		if ($struc['actsAsParanoid']) {
			$sql .= ' AND `deleted_date` IS NULL';
		}
		self::doQuery($sql);
		return;
	}

	/**
	 * Removes all records from a table and resets the auto-increment fields.
	 *
	 * @uses MysqlModel::fetchTableStructure()
	 * @uses MysqlModel::doQuery()
	 *
	 * @return void
	 */
	public static function truncate() {
		$struc = self::fetchTableStructure();
		if ($struc['actsAsParanoid']) {
			$sql = "UPDATE `{$struc['tableName']}` SET `deleted_date` = NOW()";
		} else {
			$sql = "TRUNCATE `{$struc['tableName']}`";
		}
		self::doQuery($sql);
		return;
	}

	/**
	 * Restores a record that was deleted in a table with acts as paranoid defined by a deleted_date column.
	 * If $propertyName and $searchValue are not null, it will restore only records that match those search terms, otherwise it will restore all records.
	 * If model doesn't not act as paranoid, nothing will be done.
	 *
	 * @uses MysqlModel::fetchTableStructure()
	 * @uses MysqlModel::getFieldName()
	 * @uses MysqlModel::doQuery()
	 *
	 * @param string $propertyName
	 * @param string $searchValue
	 *
	 * @return void
	 */
	public static function restore($propertyName = null, $searchValue = null) {
		$struc = self::fetchTableStructure();
		$fieldName = self::getFieldName($propertyName);
		if ($struc['actsAsParanoid'] == false) {
			return;
		}
		$sql = "UPDATE `{$struc['tableName']}` SET `deleted_date` = null";
		if ($propertyName !== null && $searchValue !== null && in_array($fieldName, $struc['fields'])) {
			$sql .= " WHERE `{$fieldName}` = '" . mysql_escape_string($searchValue) . "'";
		}
		self::doQuery($sql);
		return;
	}

	/**
	 * Performs a query and returns the result.  Uses mysqli, so you will get either a true/false or a mysqli_result object.
	 *
	 * @uses MysqliUtility::getReadOnlyConnection()
	 * @uses MysqliUtility::getReadWriteConnection()
	 *
	 * @throws Exception if there is a problem connecting or the query is malformed
	 *
	 * @param string $sql the query to run
	 * @param bool $readOnly whether or not to use a read-only connection
	 *
	 * @return mysqli_result
	 */
	public static function doQuery($sql, $readOnly = false) {
		try {
			if ($readOnly) {
				$mysqli = MysqliUtility::getReadOnlyConnection();
			} else {
				$mysqli = MysqliUtility::getReadWriteConnection();
			}
			$mysqli->autocommit(false);
			$results = $mysqli->query($sql);
			$mysqli->commit();
			$mysqli->close();
		} catch (Exception $e) {
			$mysqli->rollback();
			throw $e;
		}
		return $results;
	}

	/**
	 * Object destructor.  Will save the object if any fields are marked as dirty.
	 *
	 * @uses MysqlModel::save()
	 *
	 * @return void
	 */
	public function __destruct() {
		$this->save();
	}

	/**
	 * Overrides PHP magic method.
	 *
	 * @return string
	 */
	public function __toString() {
		return print_r($this, true);
	}

	/**
	 * Returns an array of all the properties (and types) that exist in the corresponding database table.
	 *
	 * @uses MysqlModel::doQuery()
	 * @uses StringUtility::toCamelCase()
	 * @uses MysqlModel::determinePhpType()
	 *
	 * @return array
	 */
	public function getProperties() {
		$properties = array();
		$result = self::doQuery('DESCRIBE `' . self::$tableName . '`', true);
		while ($row = $result->fetch_object()) {
			if ($row->Field == 'created_date' || $row->Field == 'deleted_date' || $row->Field == 'updated_date') {
				continue;
			}
			$name = StringUtility::toCamelCase($row->Field);
			$dbType = $row->Type;
			$type = $this->determinePhpType($dbType);
			$properties[$name] = array('description' => '', 'type' => $type);
		}
		ksort($properties);
		return $properties;
	}

	private function determinePhpType($dbType) {
		$dbType = strtolower($dbType);
		$parensStart = strpos($dbType, '(');
		$dbType = substr($dbType, 0, $parensStart);
		$integers = array (
			'tinyint',
			'smallint',
			'mediumint',
			'int',
			'integer',
			'bigint'
		);
		$floats = array (
			'double',
			'double precision',
			'real',
			'decimal',
			'numeric',
			'float'
		);
		if (in_array($dbType, $integers)) {
			$type = 'integer';
		} elseif (in_array($dbType, $floats)) {
			$type = 'float';
		} else {
			$type = 'string';
		}
		return $type;
	}

	public function getPropertiesAndValues() {
		ksort($this->properties);
		return $this->properties;
	}

	public function rewind() {
		$this->iterationIndex = 0;
		ksort($this->properties);
	}

	public function current() {
		$keys = array_keys($this->properties);
		return $this->properties[$keys[$this->iterationIndex]];
	}

	public function next() {
		$keys = array_keys($this->properties);
		$this->iterationIndex++;
		if ($this->valid()) {
			return $this->properties[$keys[$this->iterationIndex]];
		} else {
			return false;
		}
	}

	public function key() {
		$keys = array_keys($this->properties);
		return $keys[$this->iterationIndex];
	}

	public function valid() {
		$keys = array_keys($this->properties);
		return isset($keys[$this->iterationIndex]);
	}

}
